package com.hjdb88;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;

/**
 * SM2工具类
 *
 * @author hjdb88
 */
public class Sm2Util {
    /**
     * 以下为SM2推荐曲线参数
     */
    public static final BigInteger SM2_ECC_P = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16);
    public static final BigInteger SM2_ECC_A = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16);
    public static final BigInteger SM2_ECC_B = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16);
    public static final BigInteger SM2_ECC_N = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16);
    public static final BigInteger SM2_ECC_GX = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
    public static final BigInteger SM2_ECC_GY = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);

    public static final ECCurve CURVE = new ECCurve.Fp(SM2_ECC_P, SM2_ECC_A, SM2_ECC_B);
    public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY);
    public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT, SM2_ECC_N, BigInteger.ONE);

    public static final String ALGO_NAME_EC = "EC";
    public static final int SM3_DIGEST_LENGTH = 32;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private Sm2Util() {
    }

    //******************************************************************************************************************
    // 密钥

    /**
     * 生成密钥对
     *
     * @return
     * @throws Exception
     */
    public static AsymmetricCipherKeyPair generateKeyPair() {
        return generateKeyPair(DOMAIN_PARAMS, new SecureRandom());
    }

    public static AsymmetricCipherKeyPair generateKeyPair(ECDomainParameters domainParameters, SecureRandom random) {
        ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters(domainParameters, random);
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        keyGen.init(keyGenerationParams);
        return keyGen.generateKeyPair();
    }

    public static int getCurveLength(ECKeyParameters ecKey) {
        return getCurveLength(ecKey.getParameters());
    }

    public static int getCurveLength(ECDomainParameters domainParams) {
        return (domainParams.getCurve().getFieldSize() + 7) / 8;
    }

    public static byte[] convertEcPriKeyToPkcs8Der(ECPrivateKeyParameters priKey,
                                                   ECPublicKeyParameters pubKey) throws IOException {
        ECDomainParameters domainParams = priKey.getParameters();
        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
                domainParams.getN(), domainParams.getH());
        BCECPublicKey publicKey = null;
        if (pubKey != null) {
            publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
                    BouncyCastleProvider.CONFIGURATION);
        }
        BCECPrivateKey privateKey = new BCECPrivateKey(ALGO_NAME_EC, priKey, publicKey,
                spec, BouncyCastleProvider.CONFIGURATION);
        return privateKey.getEncoded();
    }

    /**
     * openssl i2d_ECPrivateKey函数生成的DER编码的ecc私钥：PKCS1标准的、带有EC_GROUP、带有公钥的
     *
     * @param priKey
     * @param pubKey
     * @return
     * @throws IOException
     */
    public static byte[] convertEcPriKeyToPkcs1Der(ECPrivateKeyParameters priKey,
                                                   ECPublicKeyParameters pubKey) throws IOException {
        byte[] pkcs8Bytes = convertEcPriKeyToPkcs8Der(priKey, pubKey);
        PrivateKeyInfo pki = PrivateKeyInfo.getInstance(pkcs8Bytes);
        ASN1Encodable encodable = pki.parsePrivateKey();
        ASN1Primitive primitive = encodable.toASN1Primitive();
        byte[] pkcs1Bytes = primitive.getEncoded();
        return pkcs1Bytes;
    }

    public static ECPrivateKeyParameters convertPkcs1DerToEcPriKey(byte[] encodedKey)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(encodedKey);
        KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
        BCECPrivateKey privateKey = (BCECPrivateKey) kf.generatePrivate(peks);
        ECParameterSpec ecParameterSpec = privateKey.getParameters();
        ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                ecParameterSpec.getG(), ecParameterSpec.getN(), ecParameterSpec.getH());
        ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(privateKey.getD(), ecDomainParameters);
        return priKey;
    }

    public static byte[] convertEcPubKeyToX509Der(ECPublicKeyParameters pubKey) {
        ECDomainParameters domainParams = pubKey.getParameters();
        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
                domainParams.getN(), domainParams.getH());
        BCECPublicKey publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
                BouncyCastleProvider.CONFIGURATION);
        return publicKey.getEncoded();
    }

    public static ECPrivateKeyParameters getPrivateKey(BigInteger privateKey) throws IOException {
        ECPrivateKeyParameters parameters = new ECPrivateKeyParameters(privateKey, DOMAIN_PARAMS);
        return parameters;
    }

    public static String getPrivateKeyStr(AsymmetricCipherKeyPair keyPair) throws IOException {
        ECPrivateKeyParameters priKey = (ECPrivateKeyParameters) keyPair.getPrivate();
        byte[] privateKeyByte = priKey.getD().toByteArray();
        return Hex.toHexString(privateKeyByte);
    }

    public static ECPublicKeyParameters getPublicKey(byte[] publicKey) {
        ECPublicKeyParameters parameters =
                new ECPublicKeyParameters(DOMAIN_PARAMS.getCurve().decodePoint(publicKey), DOMAIN_PARAMS);
        return parameters;
    }

    public static String getPublicKeyStr(AsymmetricCipherKeyPair keyPair) {
        ECPublicKeyParameters pubKey = (ECPublicKeyParameters) keyPair.getPublic();
        byte[] publicKeyByte = new byte[64];
        System.arraycopy(pubKey.getQ().getEncoded(), 1, publicKeyByte, 0, 64);
        return Hex.toHexString(publicKeyByte);
    }

    //******************************************************************************************************************
    // 加密解密

    /**
     * 公钥加密
     *
     * @param pubKey 公钥
     * @param data   源数据
     * @return SM2密文(实际包含三部分 ： ECC公钥 、 真正的密文 、 公钥和原文的SM3 - HASH值)
     * @throws InvalidCipherTextException
     */
    public static String encryt(String pubKey, String data) throws InvalidCipherTextException, UnsupportedEncodingException {
        ECPublicKeyParameters publicKey = Sm2Util.getPublicKey(Hex.decode("04" + pubKey));
        byte[] dataBytes = data.getBytes("UTF-8");
        byte[] encrytBytes = encryt(publicKey, dataBytes);
        return Base64.encodeBase64String(encrytBytes);
    }

    /**
     * 公钥加密
     *
     * @param publicKey 公钥
     * @param data      源数据
     * @return SM2密文(实际包含三部分 ： ECC公钥 、 真正的密文 、 公钥和原文的SM3 - HASH值)
     * @throws InvalidCipherTextException
     */
    public static byte[] encryt(ECPublicKeyParameters publicKey, byte[] data) throws InvalidCipherTextException {
        SM2Engine engine = new SM2Engine();
        ParametersWithRandom pwr = new ParametersWithRandom(publicKey, new SecureRandom());
        engine.init(true, pwr);
        return engine.processBlock(data, 0, data.length);
    }

    /**
     * 私钥解密
     *
     * @param priKey        私钥
     * @param sm2CipherData SM2密文(实际包含三部分：ECC公钥、真正的密文、公钥和原文的SM3-HASH值)
     * @return 原文
     * @throws InvalidCipherTextException
     */
    public static String decrypt(String priKey, String sm2CipherData) throws InvalidCipherTextException, IOException {
        ECPrivateKeyParameters privateKey = Sm2Util.getPrivateKey(new BigInteger(Hex.decode(priKey)));
        byte[] sm2CipherDataBytes = Base64.decodeBase64(sm2CipherData);
        byte[] decryptBytes = decrypt(privateKey, sm2CipherDataBytes);
        return new String(decryptBytes, "UTF-8");
    }

    /**
     * 私钥解密
     *
     * @param privateKey    私钥
     * @param sm2CipherData SM2密文(实际包含三部分：ECC公钥、真正的密文、公钥和原文的SM3-HASH值)
     * @return 原文
     * @throws InvalidCipherTextException
     */
    public static byte[] decrypt(ECPrivateKeyParameters privateKey, byte[] sm2CipherData) throws InvalidCipherTextException {
        SM2Engine engine = new SM2Engine();
        engine.init(false, privateKey);
        return engine.processBlock(sm2CipherData, 0, sm2CipherData.length);
    }

    /**
     * 分解SM2密文
     *
     * @param cipherData SM2密文
     * @return
     */
    public static Sm2Result parseSm2CipherData(byte[] cipherData) {
        int curveLength = getCurveLength(DOMAIN_PARAMS);
        return parseSm2CipherData(curveLength, SM3_DIGEST_LENGTH, cipherData);
    }

    /**
     * 分解SM2密文
     *
     * @param curveLength  曲线长度
     * @param digestLength HASH长度
     * @param cipherData   SM2密文
     * @return
     */
    public static Sm2Result parseSm2CipherData(int curveLength, int digestLength, byte[] cipherData) {
        byte[] c1 = new byte[curveLength * 2 + 1];
        System.arraycopy(cipherData, 0, c1, 0, c1.length);
        byte[] c2 = new byte[cipherData.length - c1.length - digestLength];
        System.arraycopy(cipherData, c1.length, c2, 0, c2.length);
        byte[] c3 = new byte[digestLength];
        System.arraycopy(cipherData, c1.length + c2.length, c3, 0, c2.length);
        Sm2Result result = new Sm2Result(c1, c2, c3, cipherData);
        return result;
    }

    //******************************************************************************************************************
    // 签名验签

    /**
     * 私钥签名
     *
     * @param priKey 私钥
     * @param data   源数据
     * @return 签名
     * @throws CryptoException
     */
    public static String sign(String priKey, String data) throws CryptoException, IOException {
        return sign(null, priKey, data);
    }

    /**
     * 私钥签名
     *
     * @param userId 可空，默认{1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8}
     * @param data   源数据
     * @return 签名
     * @throws CryptoException
     */
    public static String sign(String userId, String priKey, String data) throws CryptoException, IOException {
        ECPrivateKeyParameters privateKey = Sm2Util.getPrivateKey(new BigInteger(Hex.decode(priKey)));
        byte[] userIdBytes = null;
        if (StringUtils.isNotEmpty(userId)) {
            userIdBytes = userId.getBytes("UTF-8");
        }
        byte[] dataBytes = data.getBytes("UTF-8");
        byte[] signBytes = sign(userIdBytes, privateKey, dataBytes);
        return Base64.encodeBase64String(signBytes);
    }

    /**
     * 私钥签名
     *
     * @param privateKey 私钥
     * @param data       源数据
     * @return 签名
     * @throws CryptoException
     */
    public static byte[] sign(ECPrivateKeyParameters privateKey, byte[] data) throws CryptoException {
        return sign(null, privateKey, data);
    }

    /**
     * 私钥签名
     *
     * @param userId     可空，默认{1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8}
     * @param privateKey 私钥
     * @param data       源数据
     * @return 签名
     * @throws CryptoException
     */
    public static byte[] sign(byte[] userId, ECPrivateKeyParameters privateKey, byte[] data) throws CryptoException {
        SM2Signer signer = new SM2Signer();
        CipherParameters param;
        ParametersWithRandom pwr = new ParametersWithRandom(privateKey, new SecureRandom());
        if (userId != null) {
            param = new ParametersWithID(pwr, userId);
        } else {
            param = pwr;
        }
        signer.init(true, param);
        signer.update(data, 0, data.length);
        return signer.generateSignature();
    }

    /**
     * 公钥验签
     *
     * @param pubKey 公钥
     * @param data   源数据
     * @param sign   签名
     * @return 验签结果(true / false)
     */
    public static boolean verify(String pubKey, String data, String sign) throws UnsupportedEncodingException {
        return verify(null, pubKey, data, sign);
    }

    /**
     * 公钥验签
     *
     * @param userId 可空，默认{1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8}
     * @param pubKey 公钥
     * @param data   源数据
     * @param sign   签名
     * @return 验签结果(true / false)
     */
    public static boolean verify(String userId, String pubKey, String data, String sign) throws UnsupportedEncodingException {
        byte[] userIdBytes = null;
        if (StringUtils.isNotEmpty(userId)) {
            userIdBytes = userId.getBytes("UTF-8");
        }
        ECPublicKeyParameters publicKey = Sm2Util.getPublicKey(Hex.decode("04" + pubKey));
        byte[] dataBytes = data.getBytes("UTF-8");
        byte[] signBytes = Base64.decodeBase64(sign);
        return verify(userIdBytes, publicKey, dataBytes, signBytes);
    }

    /**
     * 公钥验签
     *
     * @param publicKey 公钥
     * @param data      源数据
     * @param sign      签名
     * @return 验签结果(true / false)
     */
    public static boolean verify(ECPublicKeyParameters publicKey, byte[] data, byte[] sign) {
        return verify(null, publicKey, data, sign);
    }

    /**
     * 公钥验签
     *
     * @param userId    可空，默认{1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8}
     * @param publicKey 公钥
     * @param data      源数据
     * @param sign      签名
     * @return 验签结果(true / false)
     */
    public static boolean verify(byte[] userId, ECPublicKeyParameters publicKey, byte[] data, byte[] sign) {
        SM2Signer signer = new SM2Signer();
        CipherParameters param;
        if (userId != null) {
            param = new ParametersWithID(publicKey, userId);
        } else {
            param = publicKey;
        }
        signer.init(false, param);
        signer.update(data, 0, data.length);
        return signer.verifySignature(sign);
    }

    public static void main(String[] args) throws Exception {
        AsymmetricCipherKeyPair keyPair = Sm2Util.generateKeyPair();

        String priKey = Sm2Util.getPrivateKeyStr(keyPair);
        String pubKey = Sm2Util.getPublicKeyStr(keyPair);

        System.out.println("私钥: " + priKey);
        System.out.println("公钥: " + pubKey);

        ECPrivateKeyParameters privateKey = Sm2Util.getPrivateKey(new BigInteger(Hex.decode(priKey)));
        ECPublicKeyParameters publicKey = Sm2Util.getPublicKey(Hex.decode("04" + pubKey));

        String userId = "1234567890";
        String data = "美女 Beautiful Grills 0123456789 abcdefghijklmnipq";

        String sign = Sm2Util.sign(userId, priKey, data);
        System.out.println("SM2 sign:\n" + sign);
        boolean verify = Sm2Util.verify(userId, pubKey, data, sign);
        System.out.println("SM2 verify:\n" + verify);


        String encrypt = Sm2Util.encryt(pubKey, data);
        System.out.println("SM2 encrypt:\n" + encrypt);
        String decrypt = Sm2Util.decrypt(priKey, encrypt);
        System.out.println("SM2 decrypt:\n" + decrypt);
    }
}
